﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;

namespace OperationPlugins.Reflections
{
    public static class ReflectionExtensions
    {
        #region Attributes

        private static readonly ConcurrentDictionary<MemberInfo, ConcurrentDictionary<Type, Attribute[]>> AttributeCache
            = new ConcurrentDictionary<MemberInfo, ConcurrentDictionary<Type, Attribute[]>>();

        /// <summary>
        /// Returns custom attributes applied to this member.
        /// </summary>
        /// <typeparam name="TAttribute">The type of attribute to search for.</typeparam>
        /// <param name="member">The member.</param>
        /// <returns>An array of custom attributes applied to this member, or an array with zero elements if no attributes assignable to TAttribute have been applied.</returns>
        public static IEnumerable<TAttribute> GetAttributes<TAttribute>(this MemberInfo member)
            where TAttribute : Attribute
        {
            if (member == null)
            {
                throw new ArgumentNullException("member");
            }

            Type attributeType = typeof(TAttribute);

            ConcurrentDictionary<Type, Attribute[]> subCache;
            Attribute[] attributes;

            if (AttributeCache.TryGetValue(member, out subCache))
            {
                if (subCache.TryGetValue(attributeType, out attributes))
                {
                    return (TAttribute[])attributes;
                }
            }

            attributes = (TAttribute[])member.GetCustomAttributes(attributeType, true);

            if (subCache == null)
            {
                subCache = new ConcurrentDictionary<Type, Attribute[]>();
                subCache.TryAdd(attributeType, attributes);
                AttributeCache.TryAdd(member, subCache);
                return (TAttribute[])attributes;
            }

            subCache.TryAdd(attributeType, attributes);
            return (TAttribute[])attributes;
        }

        /// <summary>
        /// Returns a custom attribute applied to this member.
        /// </summary>
        /// <typeparam name="TAttribute">The type of attribute to search for.</typeparam>
        /// <param name="member">The member.</param>
        /// <returns>A custom attribute or null if not found.</returns>
        public static TAttribute GetAttribute<TAttribute>(this MemberInfo member) where TAttribute : Attribute
        {
            if (member == null)
            {
                throw new ArgumentNullException("member");
            }

            return member.GetAttributes<TAttribute>().FirstOrDefault();
        }

        /// <summary>
        /// Checks if a custom attribute is applied to this member.
        /// </summary>
        /// <typeparam name="TAttribute">The type of attribute to search for.</typeparam>
        /// <param name="member">The member.</param>
        /// <returns>true if the specified custom attribute is applied to this member, otherwise false.</returns>
        public static bool HasAttribute<TAttribute>(this MemberInfo member) where TAttribute : Attribute
        {
            if (member == null)
            {
                throw new ArgumentNullException("member");
            }

            return member.GetAttribute<TAttribute>() != null;
        }

        #endregion

        #region Properties

        private static readonly ConcurrentDictionary<Type, PropertyInfo[]> PropertyCache =
            new ConcurrentDictionary<Type, PropertyInfo[]>();

        /// <summary>
        /// Gets all public properties of the specified type.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>All public properties.</returns>
        public static IEnumerable<PropertyInfo> GetPublicProperties(this Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            PropertyInfo[] properties;

            if (PropertyCache.TryGetValue(type, out properties))
            {
                return properties;
            }

            if (type.IsInterface)
            {
                var propertyInfos = new List<PropertyInfo>();
                var considered = new List<Type>();
                var queue = new Queue<Type>();
                considered.Add(type);
                queue.Enqueue(type);

                while (queue.Count > 0)
                {
                    var subType = queue.Dequeue();
                    foreach (var subInterface in subType.GetInterfaces())
                    {
                        if (considered.Contains(subInterface))
                        {
                            continue;
                        }
                        considered.Add(subInterface);
                        queue.Enqueue(subInterface);
                    }

                    var typeProperties = subType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

                    var newPropertyInfos = typeProperties.Where(x => !propertyInfos.Contains(x));

                    propertyInfos.InsertRange(0, newPropertyInfos);
                }

                properties = propertyInfos.ToArray();
            }
            else
            {
                properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
            }

            PropertyCache.TryAdd(type, properties);

            return properties;
        }

        public static PropertyInfo GetPublicProperty(this Type type, string propertyName)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            if (propertyName == null)
            {
                throw new ArgumentNullException("propertyName");
            }

            PropertyInfo property = type.GetPublicProperties().FirstOrDefault(x => x.Name == propertyName);
            return property;
        }

        #endregion

        #region Type Names

        private static readonly ConcurrentDictionary<Type, string> TypeNameCache =
            new ConcurrentDictionary<Type, string>();

        /// <summary>
        /// Gets the full type name.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns>The full type name.</returns>
        public static string GetFullName(this Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            string fullName;

            if (TypeNameCache.TryGetValue(type, out fullName))
            {
                return fullName;
            }

            var nullableType = Nullable.GetUnderlyingType(type);
            if (nullableType != null)
            {
                fullName = nullableType.Name + "?";
            }
            else if (type.IsGenericType == false)
            {
                fullName = string.IsNullOrWhiteSpace(type.FullName) ? type.Name : type.FullName;
            }
            else
            {
                var sb = new StringBuilder(type.FullName.Substring(0, type.FullName.IndexOf('`')));
                sb.Append("<");
                var first = true;
                foreach (var t in type.GetGenericArguments())
                {
                    if (first == false)
                    {
                        sb.Append(", ");
                    }
                    sb.Append(GetFullName(t));
                    first = false;
                }
                sb.Append(">");

                fullName = sb.ToString();
            }

            TypeNameCache.TryAdd(type, fullName);

            return fullName;
        }

        #endregion
    }
}